package org.ponly.lpf4j.impl;

import org.ponly.common.util.LazyValue;
import org.ponly.common.util.StringUtils;
import org.ponly.lpf4j.PluginDescriptor;
import org.ponly.lpf4j.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.bind.JAXB;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author vacoor
 */
public class DefaultPluginDescriptor implements PluginDescriptor {
    public static final DefaultPluginDescriptor[] EMPTY_ARRAY = new DefaultPluginDescriptor[0];
    private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginDescriptor.class);
    private final LazyValue<String> myDescription = new LazyValue<String>() {
        @Override
        protected String compute() {
            return computeDescription();
        }
    };

    private String id;
    private String name;
    private String changeNotes;
    private String version;
    private String myVendor;
    private String myVendorEmail;
    private String myVendorUrl;
    private String myVendorLogoPath;
    private String myCategory;
    private String url;
    private File myPath;
    private String[] dependencies = new String[0];
    private String[] myOptionalDependencies = new String[0];
    private Map<String, DefaultPluginDescriptor> myOptionalDescriptors;
    private boolean myDeleted;
    private ClassLoader myLoader;

    private String myDescriptionChildText;
    private String myDownloadCounter;
    private long myDate;
    private boolean myUseCoreClassLoader;
    private boolean myAllowBundledUpdate;
    private boolean myEnabled = true;
    private String mySinceBuild;
    private String myUntilBuild;
    private boolean myBundled;

    public DefaultPluginDescriptor(File pluginPath) {
        myPath = pluginPath;
    }

    @SuppressWarnings("HardCodedStringLiteral")
    private static String createDescriptionKey(final String id) {
        return "plugin." + id + ".description";
    }

    @Override
    public File getPluginRoot() {
        return myPath;
    }

    public void setPath(File path) {
        myPath = path;
    }

    public void readExternal(URL url) {
        resolve(JAXB.unmarshal(url, PluginBean.class));
    }

    public void readExternal(InputStream in) {
        resolve(JAXB.unmarshal(in, PluginBean.class));
    }

    private void resolve(PluginBean pluginBean) {
        String id = pluginBean.getId();
        String name = pluginBean.getName();

        // FIXME
        id = null != id ? id : name;
        this.id = id;
        this.name = name;
        version = pluginBean.getVersion();
        myVendorUrl = pluginBean.getUrl();
        entrance = pluginBean.getEntrance();

        // preserve items order as specified in xml (filterBadPlugins will not fail if module comes first)
        Set<String> dependentPlugins = new LinkedHashSet<String>();
        Set<String> optionalDependentPlugins = new LinkedHashSet<String>();

        String depends = pluginBean.getDepends();
        if (null != depends) {
            for (String depend : depends.split(",")) {
                if (StringUtils.hasText(depend)) {
                    dependentPlugins.add(depend);
                }
            }
        }

        this.dependencies = dependentPlugins.toArray(new String[dependentPlugins.size()]);

          /*
      if (pluginBean.dependencies != null) {
        myOptionalConfigs = new THashMap<PluginId, String>();
        for (PluginDependency dependency : pluginBean.dependencies) {
          String text = dependency.pluginId;
          if (!StringUtil.isEmpty(text)) {
            PluginId id = PluginId.getId(text);
            dependentPlugins.add(id);
            if (dependency.optional) {
              optionalDependentPlugins.add(id);
              if (!StringUtil.isEmpty(dependency.configFile)) {
                myOptionalConfigs.put(id, dependency.configFile);
              }
            }
          }
        }
      }
        */
    }

  /*
  // used in upsource
  protected void readExternal(Element element) {
    final PluginBean pluginBean = XmlSerializer.deserialize(element, PluginBean.class);

    url = pluginBean.url;
    name = pluginBean.name;
    String idString = pluginBean.id;
    if (idString == null || idString.isEmpty()) {
      idString = name;
    }
    myId = idString == null ? null : PluginId.getId(idString);

    String internalVersionString = pluginBean.formatVersion;
    if (internalVersionString != null) {
      try {
        //noinspection ResultOfMethodCallIgnored
        Integer.parseInt(internalVersionString);
      }
      catch (NumberFormatException e) {
        LOG.error(new PluginException("Invalid value in plugin.xml format version: '" + internalVersionString + "'", e, myId));
      }
    }
    myUseIdeaClassLoader = pluginBean.useIdeaClassLoader;
    myAllowBundledUpdate = pluginBean.allowBundledUpdate;
    if (pluginBean.ideaVersion != null) {
      mySinceBuild = pluginBean.ideaVersion.sinceBuild;
      myUntilBuild = convertExplicitBigNumberInUntilBuildToStar(pluginBean.ideaVersion.untilBuild);
    }

    myResourceBundleBaseName = pluginBean.resourceBundle;

    myDescriptionChildText = pluginBean.description;
    changeNotes = pluginBean.changeNotes;
    version = pluginBean.pluginVersion;
    if (version == null) {
      version = GeneralPluginManager.getBuildNumber().asStringWithoutProductCode();
    }

    myCategory = pluginBean.category;


    if (pluginBean.vendor != null) {
      myVendor = pluginBean.vendor.name;
      myVendorEmail = pluginBean.vendor.email;
      myVendorUrl = pluginBean.vendor.url;
      myVendorLogoPath = pluginBean.vendor.logo;
    }

    // preserve items order as specified in xml (filterBadPlugins will not fail if module comes first)
    Set<PluginId> dependentPlugins = new LinkedHashSet<PluginId>();
    Set<PluginId> optionalDependentPlugins = new LinkedHashSet<PluginId>();
    if (pluginBean.dependencies != null) {
      myOptionalConfigs = new THashMap<PluginId, String>();
      for (PluginDependency dependency : pluginBean.dependencies) {
        String text = dependency.pluginId;
        if (!StringUtil.isEmpty(text)) {
          PluginId id = PluginId.getId(text);
          dependentPlugins.add(id);
          if (dependency.optional) {
            optionalDependentPlugins.add(id);
            if (!StringUtil.isEmpty(dependency.configFile)) {
              myOptionalConfigs.put(id, dependency.configFile);
            }
          }
        }
      }
    }

    dependencies = dependentPlugins.isEmpty() ? PluginId.EMPTY_ARRAY : dependentPlugins.toArray(new PluginId[dependentPlugins.size()]);
    myOptionalDependencies = optionalDependentPlugins.isEmpty() ? PluginId.EMPTY_ARRAY : optionalDependentPlugins.toArray(new PluginId[optionalDependentPlugins.size()]);

  */

    private static final Pattern EXPLICIT_BIG_NUMBER_PATTERN = Pattern.compile("(.*)\\.(9{4,}+|10{4,}+)");

    /**
     * Convert build number like '146.9999' to '146.*' (like plugin repository does) to ensure that plugins which have such values in
     * 'until-build' attribute will be compatible with 146.SNAPSHOT build.
     */
    public static String convertExplicitBigNumberInUntilBuildToStar(String build) {
        if (build == null) return null;
        Matcher matcher = EXPLICIT_BIG_NUMBER_PATTERN.matcher(build);
        if (matcher.matches()) {
            return matcher.group(1) + ".*";
        }
        return build;
    }

    @Override
    public String getDescription() {
//        return myDescription.getValue();
        return null;
    }

    @Override
    public String getChangeNotes() {
        return changeNotes;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String[] getDependentPluginIds() {
        return dependencies;
    }

    private String entrance;

    @Override
    public String getEntrance() {
        return entrance;
    }

    @Override
    public String[] getOptionalDependentPluginIds() {
        // TODO 没用?
        return myOptionalDependencies;
    }

    @Override
    public String getVendor() {
        return myVendor;
    }

    public void setVendor(final String val) {
        myVendor = val;
    }

    @Override
    public String getVersion() {
        return version;
    }

    @SuppressWarnings("HardCodedStringLiteral")
    public List<File> getClassPath() {
        /*-
         * 插件打包说明
         * 1. 需要解压执行
         *    plugin-archive.jar(zip)
         *       | - META-INF
         *            | - plugin.xml (也可以打包为jar放到lib下)
         *       | - classes
         *       | - lib (依赖jar或 native lib)
         * 2. 不需要解压执行的
         *    plugin-archive.jar(zip)
         *       | - META-INF
         *            | - plugin.xml
         *       | - (package)
         */
        if (!myPath.isDirectory()) {
            return Collections.singletonList(myPath);
        } else {
            final List<File> result = new ArrayList<File>();
            final File classesDir = new File(myPath, "classes");

            if (classesDir.exists()) {
                result.add(classesDir);
            }

            final File[] libs = new File(myPath, "lib").listFiles();
            if (null != libs && 0 < libs.length) {
                for (File lib : libs) {
                    if (lib.isDirectory() || Utils.isJarOrZip(lib)) {
                        result.add(lib);
                    }
                }
            }
            return result;
        }
    }


    @Override
    public String getVendorEmail() {
        return myVendorEmail;
    }

    public void setVendorEmail(final String val) {
        myVendorEmail = val;
    }

    @Override
    public String getVendorUrl() {
        return myVendorUrl;
    }

    public void setVendorUrl(final String val) {
        myVendorUrl = val;
    }

    @Override
    public String getUrl() {
        return url;
    }

    public void setUrl(final String val) {
        url = val;
    }

    public String toString() {
        return "DefaultPluginDescriptor[name='" + name + "', classpath='" + myPath + "']";
    }

    public boolean isDeleted() {
        return myDeleted;
    }

    public void setDeleted(boolean deleted) {
        myDeleted = deleted;
    }

    public void setLoader(ClassLoader loader) {
        myLoader = loader;
    }

    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof DefaultPluginDescriptor)) return false;

        final DefaultPluginDescriptor pluginDescriptor = (DefaultPluginDescriptor) o;

        return name == null ? pluginDescriptor.name == null : name.equals(pluginDescriptor.name);
    }

    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }

  /*
  @Override
  public HelpSetPath[] getHelpSets() {
    return myHelpSets;
  }
  */

    /*
       This setter was explicitly defined to be able to set downloads count for a
       descriptor outside its loading from the xml file since this information
       is available only from the site.
    */
    public void setDownloadsCount(String downloadsCount) {
        myDownloadCounter = downloadsCount;
    }

    @Override
    public String getDownloads() {
        return myDownloadCounter;
    }

    public long getDate() {
        return myDate;
    }

    /*
       This setter was explicitly defined to be able to set date for a
       descriptor outside its loading from the xml file since this information
       is available only from the site.
    */
    public void setDate(long date) {
        myDate = date;
    }

    @Override
    public ClassLoader getPluginClassLoader() {
        return myLoader != null ? myLoader : getClass().getClassLoader();
    }

    @Override
    public String getVendorLogoPath() {
        return myVendorLogoPath;
    }

    public void setVendorLogoPath(final String vendorLogoPath) {
        myVendorLogoPath = vendorLogoPath;
    }

    public boolean isUseCoreClassLoader() {
        return myUseCoreClassLoader;
    }

    public void setUseCoreClassLoader(final boolean useCoreClassLoader) {
        myUseCoreClassLoader = useCoreClassLoader;
    }

    private String computeDescription() {
        /*
        ResourceBundle bundle = null;
        if (myResourceBundleBaseName != null) {
            try {
                bundle = AbstractBundle.getResourceBundle(myResourceBundleBaseName, getPluginClassLoader());
            } catch (MissingResourceException e) {
                LOG.info("Cannot find plugin " + myId + " resource-bundle: " + myResourceBundleBaseName);
            }
        }

        if (bundle == null) {
            return myDescriptionChildText;
        }

        return CommonBundle.messageOrDefault(bundle, createDescriptionKey(myId), myDescriptionChildText == null ? "" : myDescriptionChildText);
        */
        return null;
    }

    /*
    public void insertDependency(PluginDescriptor d) {
        PluginId[] deps = new PluginId[getDependentPluginIds().length + 1];
        deps[0] = d.getPluginId();
        System.arraycopy(dependencies, 0, deps, 1, deps.length - 1);
        dependencies = deps;
    }
    */

    @Override
    public boolean isEnabled() {
        return myEnabled;
    }

    public void setEnabled(final boolean enabled) {
        myEnabled = enabled;
    }

    @Override
    public String getSinceBuild() {
        return mySinceBuild;
    }

    @Override
    public String getUntilBuild() {
        return myUntilBuild;
    }

    Map<String, DefaultPluginDescriptor> getOptionalDescriptors() {
        return myOptionalDescriptors;
    }

    void setOptionalDescriptors(Map<String, DefaultPluginDescriptor> optionalDescriptors) {
        myOptionalDescriptors = optionalDescriptors;
    }

    void mergeOptionalConfig(final DefaultPluginDescriptor descriptor) {
    /*
    if (myExtensions == null) {
      myExtensions = descriptor.myExtensions;
    }
    else if (descriptor.myExtensions != null) {
      myExtensions.putAllValues(descriptor.myExtensions);
    }

    if (myExtensionsPoints == null) {
      myExtensionsPoints = descriptor.myExtensionsPoints;
    }
    else if (descriptor.myExtensionsPoints != null) {
      myExtensionsPoints.putAllValues(descriptor.myExtensionsPoints);
    }

    if (myActionsElements == null) {
      myActionsElements = descriptor.myActionsElements;
    }
    else if (descriptor.myActionsElements != null) {
      myActionsElements.addAll(descriptor.myActionsElements);
    }

    myAppComponents = mergeComponents(myAppComponents, descriptor.myAppComponents);
    myProjectComponents = mergeComponents(myProjectComponents, descriptor.myProjectComponents);
    myModuleComponents = mergeComponents(myModuleComponents, descriptor.myModuleComponents);
    */
    }

    @Override
    public boolean isBundled() {
        return myBundled;
    }

    public void setBundled(boolean bundled) {
        this.myBundled = bundled;
    }

    @Override
    public boolean allowBundledUpdate() {
        return myAllowBundledUpdate;
    }

    @Override
    public String getId() {
        return id;
    }
}